package com.hb.sq3d;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.SurfaceTexture;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Size;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;

import com.hb.sq3d.vos.LineArg;
import com.hb.sq3d.vos.ModelArg;
import com.hb.sq3d.vos.Status;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

public class Viewer3D extends RelativeLayout {
    private static final boolean UseDefaultJniLibs = true;// for debug sake -- true: load 'libsq3d.so' from `jniLibs` or 'AAR(jni)'

    static {
        if (UseDefaultJniLibs)
            System.loadLibrary("sq3d");
    }

    /** public methods */
    public static void setVersion(String version) {
        s_version = version;
    }

    public static void setComingModels(List<ModelArg> models, long timeMs, int number) {
        if (s_debugRecvModelInfoTv != null && s_debugRecvModelInfoTv.getVisibility() == View.VISIBLE) {
            doInMainThread(() -> {
                String timeStr = s_dateFormat.format(new Date(timeMs));
                s_debugRecvModelInfoTv.setText("Targets: [" + timeStr + "] #" + number);
            });
        }
        if (models.isEmpty() || ProjectTag.isEmpty())
            return;
        recvModels(ModelArg.getJNIDataFromModels(models, ProjectTag.equals("IsProject_SSQK")), timeMs, number);
    }
    public static void setComingLines(List<LineArg> lines, long timeMs, int number, String tag) {
        if (s_debugRecvLineInfoTv != null && s_debugRecvLineInfoTv.getVisibility() == View.VISIBLE) {
            doInMainThread(() -> {
                String timeStr = s_dateFormat.format(new Date(timeMs));
                s_debugRecvLineInfoTv.setText(" Lines: [" + timeStr + "] #" + number + " (" + tag + ")");
            });
        }
        if (lines.isEmpty())
            return;
        recvLines(LineArg.getJNIDataFromLines(lines));
    }
    public static void setComingStatus(Status status) {
        if (s_status != status) {
            Log.i("Viewer3D", "recvStatus: " + status);
            s_status = status;
        }
        recvStatus(status);
    }
    public static void switchDayNightMode(boolean isNightMode) {
        s_isCurrentNightMode = isNightMode;
        setBackground(isNightMode);
        changeDarkMode(isNightMode);
    }
    public static void setVisible(View view, boolean visible) {
        boolean isOriginVisible = view.getVisibility() == View.VISIBLE;
        if ((isOriginVisible && visible) || (!isOriginVisible && !visible))
            return;
        setReceivingDataEnabled(visible);
        view.setVisibility(visible ? View.VISIBLE : View.GONE);

        s_isVisible = visible;
    }

    public static void setView3DInitCompletedListener(OnView3DInitCompleteListener listener) {
        if (s_isView3DInitCompleted) {
            listener.onView3DInitCompleted();
            return;
        }
        s_view3DInitCompleteListener = listener;
    }

    private static native String[] onCreate(AssetManager assetManager, String internalPath, String externalPath);
    private static native int[] getSize();
    private static native void createEGLContext(SurfaceTexture surfaceTexture);
    private static native void destroyEGLContext();
    private static native boolean isEGLDestroyed();
    private static native boolean isRendering();
    private static native float frameRate();
    private static native void setReceivingDataEnabled(boolean enabled);
    private static native void onViewportChanged(int width, int height);
    private static native int frameUpdate();
    private static native void changeDarkMode(boolean isDarkMode);
    private static native void recvModels(ModelArg.JNIData allModelArgs, long timeMs, int number);
    private static native void recvLines(LineArg.JNIData allLineArgs);
    private static native void recvStatus(Status status);

    /* *************************************************************/
    /* *************************************************************/
    public static String ProjectTag = "";
    public static boolean NativeReleased = true;
    private static final int FRAME_UPDATE_STATUS_ZHICHE_RENDERED_DONE = 0x100;

    private static String s_version;
    private static ViewGroup s_instance;
    private static boolean s_isCurrentNightMode = false;// default: day mode
    private static String s_bgLightImgAssetPath, s_bgDarkImgAssetPath;
    private static ImageView s_backgroundIv;
    private ImageView m_zhicheImage;
    private TextureView m_textureView;
    private static SimpleDateFormat s_dateFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault());
    private static boolean s_isVisible = false;

    private TextureView.SurfaceTextureListener m_surfaceTextureListener;
    private SurfaceTexture m_surfaceTexture;
    private Size m_surfaceSize;
    private int m_restartRenderingTimes = 0;

    private static TextView s_debugCurrentTimeMsTv, s_debugRecvModelInfoTv, s_debugRecvLineInfoTv, s_debugVersionTv;

    private static boolean s_isView3DInitCompleted = false;

    private static Status s_status;

    private int m_clickCount;
    private long m_lastClickTimeMs;

    private static OnView3DInitCompleteListener s_view3DInitCompleteListener;
    public interface OnView3DInitCompleteListener {  void onView3DInitCompleted(); }

    public Viewer3D(Context context) {
        super(context);
        init();
    }

    public Viewer3D(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public Viewer3D(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        Log.i("Viewer3D", "init.............................");

        if (!UseDefaultJniLibs)
            loadExternalSo(getContext(), "/sdcard/libsq3d.so", "/sdcard/libsq3d_debug.so");

        s_instance = this;
        switchDayNightMode(s_isCurrentNightMode);

        // background image(z-index:N)
        s_backgroundIv = new ImageView(getContext());
        s_backgroundIv.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        addView(s_backgroundIv);

        // TextureView(z-index:N+1)
        m_textureView = new TextureView(getContext());
        LayoutParams textureParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        textureParams.addRule(CENTER_IN_PARENT, TRUE);
        m_textureView.setLayoutParams(textureParams);
        addView(m_textureView);

        // top image(z-index:N+2)
        m_zhicheImage = new ImageView(getContext());
        LayoutParams bottomParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        bottomParams.addRule(ALIGN_PARENT_BOTTOM, TRUE);
        bottomParams.addRule(CENTER_HORIZONTAL, TRUE);
        m_zhicheImage.setLayoutParams(bottomParams);
        m_zhicheImage.setVisibility(View.GONE);
        addView(m_zhicheImage);

        /* ************************* debug info views ***************************/
        s_debugCurrentTimeMsTv = new TextView(getContext());
        s_debugCurrentTimeMsTv.setId(View.generateViewId());
        s_debugCurrentTimeMsTv.setText("Time");
        s_debugCurrentTimeMsTv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 30);
        s_debugCurrentTimeMsTv.setTextColor(Color.RED);
        LayoutParams topParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        topParams.addRule(ALIGN_PARENT_TOP, TRUE);
        topParams.addRule(CENTER_HORIZONTAL, TRUE);
        s_debugCurrentTimeMsTv.setLayoutParams(topParams);
        addView(s_debugCurrentTimeMsTv);
        s_debugCurrentTimeMsTv.setVisibility(View.GONE);

        s_debugRecvModelInfoTv = new TextView(getContext());
        s_debugRecvModelInfoTv.setId(View.generateViewId());
        s_debugRecvModelInfoTv.setText("No Targets Received");
        s_debugRecvModelInfoTv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 25);
        s_debugRecvModelInfoTv.setTextColor(Color.BLUE);
        LayoutParams modelInfoParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        modelInfoParams.addRule(BELOW, s_debugCurrentTimeMsTv.getId());// BELOW
        modelInfoParams.addRule(CENTER_HORIZONTAL, TRUE);
        s_debugRecvModelInfoTv.setLayoutParams(modelInfoParams);
        addView(s_debugRecvModelInfoTv);
        s_debugRecvModelInfoTv.setVisibility(View.GONE);

        s_debugRecvLineInfoTv = new TextView(getContext());
        s_debugRecvLineInfoTv.setText(" No Lines Received");
        s_debugRecvLineInfoTv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 25);
        s_debugRecvLineInfoTv.setTextColor(Color.BLUE);
        LayoutParams lineInfoParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        lineInfoParams.addRule(BELOW, s_debugRecvModelInfoTv.getId());// BELOW
        lineInfoParams.addRule(CENTER_HORIZONTAL, TRUE);
        s_debugRecvLineInfoTv.setLayoutParams(lineInfoParams);
        addView(s_debugRecvLineInfoTv);
        s_debugRecvLineInfoTv.setVisibility(View.GONE);

        s_debugVersionTv = new TextView(getContext());
        s_debugVersionTv.setText("Version");
        s_debugVersionTv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 25);
        s_debugVersionTv.setTextColor(Color.BLUE);
        LayoutParams verParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        verParams.addRule(ALIGN_PARENT_BOTTOM, TRUE);
        verParams.addRule(ALIGN_PARENT_LEFT, TRUE);
        s_debugVersionTv.setLayoutParams(verParams);
        addView(s_debugVersionTv);
        s_debugVersionTv.setVisibility(View.GONE);

        /* ********************************************************************************/
        /* ********************************************************************************/
        String[] retDatas = initAssets();
        ProjectTag = retDatas[0];
        NativeReleased = Boolean.parseBoolean(retDatas[1]);
        s_bgLightImgAssetPath = retDatas[2];
        s_bgDarkImgAssetPath = retDatas[3];
        String zhichePngSdcardPath = retDatas[4];

        m_surfaceTextureListener = new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
                Log.i("Viewer3D", "onSurfaceTextureAvailable: " + width + " x " + height + " (" + surfaceTexture + ")");
                m_surfaceTexture = surfaceTexture;
                m_surfaceSize = new Size(width, height);
                s_isVisible = true;

                doInMainThread(()->{
                    m_zhicheImage.setVisibility(View.VISIBLE);
                    setBackground(s_isCurrentNightMode);
                    setLoadingImage(zhichePngSdcardPath);
                });

                new Thread(() -> {// start a RenderThread
                    Thread.currentThread().setPriority(Thread.MAX_PRIORITY);// Set thread priority to the highest
                    createEGLContext(surfaceTexture);
                }).start();
            }

            @Override
            public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surfaceTexture) {
                Log.w("Viewer3D", "surfaceDestroyed (" + surfaceTexture + ")");
                destroyEGLContext();
                return true;
            }

            @Override
            public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
                Log.i("Viewer3D", "onSurfaceTextureSizeChanged: " + width + " x " + height);
                onViewportChanged(width, height);
            }

            @Override
            public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surfaceTexture) {
                int status = frameUpdate();
                switch (status) {
                    case FRAME_UPDATE_STATUS_ZHICHE_RENDERED_DONE:
                        if (m_zhicheImage.getVisibility() != View.GONE) {
                            m_zhicheImage.setVisibility(View.GONE);
                            Log.i("Viewer3D", "ZhichePng disappeared (ZhicheEntity has rendered done!)");
                        }
                        break;
                }
                if (s_debugCurrentTimeMsTv != null && s_debugCurrentTimeMsTv.getVisibility() == View.VISIBLE) {
                    doInBgThread(() -> {
                        String timeStr = s_dateFormat.format(new Date(System.currentTimeMillis()));
                        float rate = frameRate();
                        if (rate > 0 && rate < 1000)
                            timeStr += String.format("  (%.1fHz)", rate);
                        String time_rate_str = timeStr;
                        s_debugCurrentTimeMsTv.post(() -> { s_debugCurrentTimeMsTv.setText(time_rate_str); });
                    });
                }
            }
        };
        m_textureView.setOpaque(false);// make support alpha channel!
        m_textureView.setSurfaceTextureListener(m_surfaceTextureListener);

        s_isView3DInitCompleted = true;
        if (s_view3DInitCompleteListener != null)
            s_view3DInitCompleteListener.onView3DInitCompleted();

        // keep rendering alive in no-exception-occur situation!
        keepRenderingActive();

        /* *******************************************************************/
        addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(@NonNull View v) {
                Log.i("Viewer3D", "onViewAttachedToWindow");
            }

            @Override
            public void onViewDetachedFromWindow(@NonNull View v) {
                Log.i("Viewer3D", "onViewDetachedFromWindow");
            }
        });

        setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        return true;// make event consumed, then can recv ACTION_UP event
                    case MotionEvent.ACTION_UP: {
                        float rightZoneStartX = v.getWidth() * 0.75f;// right-bottom 1/4 part
                        float rightZoneStartY = v.getHeight() * 0.75f;
                        float leftZoneEndX = v.getWidth() * 0.25f;// left-bottom 1/4
                        if (event.getY() >= rightZoneStartY) {// bottom 1/4
                            boolean isRightCorner = event.getX() >= rightZoneStartX;
                            boolean isLeftCorner = event.getX() <= leftZoneEndX;
                            if (isRightCorner || isLeftCorner) {// right or left 1/4
                                long clickTimeMs = System.currentTimeMillis();
                                if (clickTimeMs - m_lastClickTimeMs > 1000) {
                                    m_clickCount = 0;
                                } else {
                                    m_clickCount++;
                                    if (m_clickCount > 10) {
                                        m_clickCount = 0;
                                        if (isRightCorner) {
                                            if (s_debugCurrentTimeMsTv.getVisibility() == View.GONE) {
                                                s_debugCurrentTimeMsTv.setVisibility(View.VISIBLE);
                                                s_debugRecvModelInfoTv.setVisibility(View.VISIBLE);
                                                s_debugRecvLineInfoTv.setVisibility(View.VISIBLE);
                                            } else {
                                                s_debugCurrentTimeMsTv.setVisibility(View.GONE);
                                                s_debugRecvModelInfoTv.setVisibility(View.GONE);
                                                s_debugRecvLineInfoTv.setVisibility(View.GONE);
                                            }
                                        } else {// isLeftCorner
                                            if (s_debugVersionTv.getVisibility() == View.GONE) {
                                                s_debugVersionTv.setText(s_version);
                                                s_debugVersionTv.setVisibility(View.VISIBLE);
                                            } else
                                                s_debugVersionTv.setVisibility(View.GONE);
                                        }
                                    }
                                }
                                m_lastClickTimeMs = System.currentTimeMillis();
                            }
                        }
                        break;
                    }
                }
                return false;// don't handle other areas
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int[] size = getSize();
        int w = size[0], h = size[1];
        if (s_debugCurrentTimeMsTv == null || s_debugCurrentTimeMsTv.getVisibility() == View.GONE)
            Log.i("Viewer3D", "onMeasure => " + w + "x" + h);
        super.onMeasure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY));
    }

    private String[] initAssets() {
        String internalPath = getContext().getFilesDir().getAbsolutePath();
        File[] externalDirs = getContext().getExternalFilesDirs(null);
        String externalPath = (externalDirs.length > 0 && externalDirs[0] != null) ? externalDirs[0].getAbsolutePath() : null;
        return onCreate(getContext().getAssets(), internalPath, externalPath);
    }

    private static void setBackground(boolean isDarkMode) {
        String imgAssetPath = isDarkMode ? s_bgDarkImgAssetPath : s_bgLightImgAssetPath;
        if (imgAssetPath == null)// first time may not ready
            return;
        try {
            InputStream inputStream = s_instance.getContext().getAssets().open(imgAssetPath);
            Bitmap bgBmp = BitmapFactory.decodeStream(inputStream);
            s_backgroundIv.setImageBitmap(bgBmp);
            inputStream.close();
        } catch (Exception e) {
            Log.e("Viewer3D", "isDarkMode:" + isDarkMode + ", background image set fail!", e);
        }
    }

    private void setLoadingImage(String sdcardImagePath) {
        Bitmap bmp = BitmapFactory.decodeFile(sdcardImagePath);
        if (bmp == null) {
            Log.w("Viewer3D", "setLoadingImage: " + sdcardImagePath + " no exists! to get from 'assets'....");
            String zhichePngAssetsPath = "";
            if (ProjectTag.equals("IsProject_SQ"))
                zhichePngAssetsPath = "res3D/asset/cache/zhiche.png";
            else if (ProjectTag.equals("IsProject_SSQK"))
                zhichePngAssetsPath = "res3D/asset/object/2d/Zhiche.png";
            try {
                InputStream is = s_instance.getContext().getAssets().open(zhichePngAssetsPath);
                bmp = BitmapFactory.decodeStream(is);
                is.close();
            } catch (IOException e) {
                Log.w("Viewer3D", "setLoadingImage: " + zhichePngAssetsPath + " fail!", e);
            }
        }
        if (bmp != null) {
            m_zhicheImage.setImageBitmap(bmp);
            m_zhicheImage.setVisibility(View.VISIBLE);
            Log.i("Viewer3D", "setLoadingImage(zhichePNG) success!");
        }
    }

    private void keepRenderingActive() {
        doInBgThread(new Runnable() {
            @Override
            public void run() {
                if (s_isVisible && (!isRendering() || isEGLDestroyed())) {
                    Log.w("Viewer3D", "restartRendering...... manually! (times:" + ++m_restartRenderingTimes + ")");
                    m_surfaceTextureListener.onSurfaceTextureDestroyed(m_surfaceTexture);
                    while (!isEGLDestroyed()) ;// blocking
                    m_surfaceTextureListener.onSurfaceTextureAvailable(m_surfaceTexture, m_surfaceSize.getWidth(), m_surfaceSize.getHeight());
                    Log.w("Viewer3D", "restartRendering (times:" + m_restartRenderingTimes + ") successfully!");
                }
                postDelay(this, 100);
            }
        });
    }

    private static void fadeIn(View view, int duringMs, Runnable onAnimationEnd) {// alpha: from 0 to 1
        view.setAlpha(0f);// src alpha=0
        view.animate().alpha(1f)// dest alpha=1
                .setDuration(duringMs)
                .setInterpolator(new AccelerateInterpolator())
                .withEndAction(onAnimationEnd)
                .start();
    }
    private static void fadeOut(View view, int duringMs, Runnable onAnimationEnd) {// alpha: from 1 to 0
        view.setAlpha(1f);// src alpha=1
        view.animate().alpha(0f)// dest alpha=0
                .setDuration(duringMs)
                .setInterpolator(new AccelerateInterpolator())
                .withEndAction(onAnimationEnd)
                .start();
    }

    @SuppressLint("UnsafeDynamicallyLoadedCode")
    private void loadExternalSo(Context context, String releaseSoPath, String debugSoPath) {
        File soFile = new File(releaseSoPath);// "/sdcard/libsq3d.so"
        if (!soFile.exists())
            soFile = new File(debugSoPath);// "/sdcard/libsq3d_debug.so"
        if (!soFile.exists()) {
            Log.e("Viewer3D", "loadExternalSo fail! " + releaseSoPath + " and " + debugSoPath + " are not exists!");
            return;
        }
        String fileName = soFile.getAbsolutePath().substring(soFile.getAbsolutePath().lastIndexOf("/") + 1);
        File privateDir = context.getFilesDir();// private directory
        File targetFile = new File(privateDir, fileName);
        // assume the storage permission is granted
        try {
            // copy *.so to private directory
            InputStream in = Files.newInputStream(soFile.toPath());
            OutputStream out = Files.newOutputStream(targetFile.toPath());
            byte[] buffer = new byte[1024];
            int length;
            while ((length = in.read(buffer)) > 0)
                out.write(buffer, 0, length);
            in.close();
            out.close();
            // load *.so from private directory
            System.load(targetFile.getAbsolutePath());// use `load()` instead of `loadLibrary()`
            Log.i("Viewer3D", "loadExternalSo: '" + soFile.getAbsolutePath() + "' is loading...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /* *************************************************************/
    /* *************************************************************/
    private static Handler s_bgThreadHandler, s_mainThreadHandler;
    private static void doInBgThread(Runnable r) {
        if (s_bgThreadHandler == null) {
            HandlerThread thread = new HandlerThread("Default Thread", android.os.Process.THREAD_PRIORITY_BACKGROUND);
            thread.start();
            s_bgThreadHandler = new Handler(thread.getLooper());
        }
        s_bgThreadHandler.post(r);
    }
    private static void postDelay(Runnable r, long delayMs) {
        if (s_bgThreadHandler == null) {
            HandlerThread thread = new HandlerThread("Default Thread", android.os.Process.THREAD_PRIORITY_BACKGROUND);
            thread.start();
            s_bgThreadHandler = new Handler(thread.getLooper());
        }
        s_bgThreadHandler.postDelayed(r, delayMs);
    }

    private static void doInMainThread(Runnable r) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            r.run();
            return;
        }
        if (s_mainThreadHandler == null)
            s_mainThreadHandler = new Handler(Looper.getMainLooper());
        s_mainThreadHandler.post(r);
    }
    public static void postUIDelay(Runnable r, long delayMs) {
        if (s_mainThreadHandler == null)
            s_mainThreadHandler = new Handler(Looper.getMainLooper());
        if (delayMs == 0)
            s_mainThreadHandler.post(r);
        else
            s_mainThreadHandler.postDelayed(r, delayMs);
    }
}